/*
 * Copyright 2002-2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.i360day.invoker;

import com.i360day.invoker.codes.encoder.Encoder;
import com.i360day.invoker.common.HttpInvokerRemoteConstant;
import com.i360day.invoker.common.StringUtils;
import com.i360day.invoker.exception.HttpInvokerException;
import com.i360day.invoker.exception.InvalidAcceptTimestampException;
import com.i360day.invoker.exception.InvalidContentTypeException;
import com.i360day.invoker.exception.InvalidSignatureException;
import com.i360day.invoker.security.HttpInvokerSecurityAdapter;
import com.i360day.invoker.support.RemoteInvocation;
import com.i360day.invoker.support.RemoteInvocationFactory;
import com.i360day.invoker.support.RemoteInvocationResult;
import com.i360day.invoker.support.RemoteInvocationSerializingExporter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpRequestHandler;

import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;

/**
 * Servlet-API-based HTTP request handler that exports the specified service bean
 * as HTTP invoker service endpoint, accessible via an HTTP invoker proxy.
 *
 * <p>Deserializes remote invocation objects and serializes remote invocation
 * result objects. Uses Java serialization just like RMI, but provides the
 * same ease of setup as Caucho's HTTP-based Hessian protocol.
 *
 * <p><b>HTTP invoker is the recommended protocol for Java-to-Java remoting.</b>
 * It is more powerful and more extensible than Hessian, at the expense of
 * being tied to Java. Nevertheless, it is as easy to set up as Hessian,
 * which is its main advantage compared to RMI.
 *
 * <p><b>WARNING: Be aware of vulnerabilities due to unsafe Java deserialization:
 * Manipulated input streams could lead to unwanted code execution on the server
 * during the deserialization step. As a consequence, do not expose HTTP invoker
 * endpoints to untrusted clients but rather just between your own services.</b>
 * In general, we strongly recommend any other message format (e.g. JSON) instead.
 *
 * @author liju.z
 * @see HttpInvokerClientFactoryBean
 * @since 1.1
 */
public class HttpInvokerServiceExporter extends RemoteInvocationSerializingExporter implements HttpRequestHandler {
    private final Encoder encoder;
    private final RemoteInvocationFactory remoteInvocationFactory;
    private final HttpInvokerSecurityAdapter httpInvokerSecurityAdapter;
    /**
     * accept 接受值
     */
    private MediaType acceptMediaType = MediaType.valueOf(HttpInvokerRemoteConstant.ACCEPT_RPC_HTTP_INVOKER);

    /**
     * constructor
     *
     * @param encoder
     * @param remoteInvocationFactory
     * @param httpInvokerSecurityAdapter
     */
    public HttpInvokerServiceExporter(Encoder encoder,
                                      RemoteInvocationFactory remoteInvocationFactory,
                                      HttpInvokerSecurityAdapter httpInvokerSecurityAdapter) {
        this.encoder = encoder;
        this.remoteInvocationFactory = remoteInvocationFactory;
        this.httpInvokerSecurityAdapter = httpInvokerSecurityAdapter;
    }

    /**
     * Reads a remote invocation from the request, executes it,
     * and writes the remote invocation result to the response.
     *
     * @see #readRemoteInvocation(HttpServletRequest)
     * @see #invokeAndCreateResult(RemoteInvocation, Object)
     * @see Encoder#writeRemoteInvocationResult(HttpServletResponse, Type, RemoteInvocationResult)
     */
    @Override
    public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {
        //default
        RemoteInvocationResult result;
        Type returnType = null;
        try {
            //验证请求时间有效性
            verifyTimestamp(request, response);
            //接受头信息
            checkAccept(request, response);
            //read
            RemoteInvocation invocation = readRemoteInvocation(request);
            returnType = invocation.getReturnType(getService());
            //sign verify
            signVerify(request, response, invocation);
            //check
            httpInvokerSecurityAdapter.check(request, response, invocation);
            //invoke
            result = invokeAndCreateResult(invocation, getProxy());
        } catch (Throwable ex) {
            logger.error("http invoker service handler error {}", ex);
            result = new RemoteInvocationResult(ex);
        }
        //write
        encoder.writeRemoteInvocationResult(response, returnType == null ? Object.class : returnType, result);
    }

    /**
     * 检查头信息
     *
     * @param request
     * @throws HttpMediaTypeNotAcceptableException
     */
    private void checkAccept(HttpServletRequest request, HttpServletResponse response) throws HttpMediaTypeNotAcceptableException {
        Enumeration<String> headers = request.getHeaders(HttpHeaders.ACCEPT);
        List<String> headerValues = new LinkedList<>();
        while (headers.hasMoreElements()) {
            headerValues.add(headers.nextElement());
        }
        List<MediaType> mediaTypes = MediaType.parseMediaTypes(headerValues);
        MediaType.sortBySpecificityAndQuality(mediaTypes);
        if (!mediaTypes.contains(acceptMediaType)) {
            response.setStatus(HttpStatus.FORBIDDEN.value());
            throw new HttpMediaTypeNotAcceptableException("Could not parse 'Accept' header " + headerValues + ": accept is " + acceptMediaType);
        }
    }

    /**
     * 验证签名
     *
     * @param request
     * @param response
     * @param invocation
     */
    private void signVerify(HttpServletRequest request, HttpServletResponse response, RemoteInvocation invocation) throws NoSuchMethodException {
        Method method = invocation.getMethod(getService());
//        String signValue = HttpInvokerRemoteConstant.sign(invocation.getMethodName(), method.getReturnType(), invocation.getArguments());
        String signValue = HttpInvokerRemoteConstant.sign(method, invocation.getArguments());
        if (!StringUtils.isEquals(signValue, request.getHeader(HttpInvokerRemoteConstant.HTTP_INVOKER_AUTHORIZATION_KEY))) {
            response.setStatus(HttpStatus.FORBIDDEN.value());
            throw new InvalidSignatureException("Invalid signature " + signValue);
        }
    }

    /**
     * 验证时间有效性
     *
     * @param request
     * @param response
     */
    private void verifyTimestamp(HttpServletRequest request, HttpServletResponse response) {
        String acceptTimestamp = request.getHeader(HttpInvokerRemoteConstant.ACCEPT_TIMESTAMP);
        if (StringUtils.isEmpty(acceptTimestamp)) {
            response.setStatus(HttpStatus.FORBIDDEN.value());
            throw new InvalidAcceptTimestampException(String.format("Invalid %s", HttpInvokerRemoteConstant.ACCEPT_TIMESTAMP));
        }
        //验证当前请求是否在有效时间内
        long requestTimestamp = Long.valueOf(acceptTimestamp);
        if ((System.currentTimeMillis() - (5 * 1000 * 60)) > requestTimestamp) {
            response.setStatus(HttpStatus.FORBIDDEN.value());
            throw new InvalidAcceptTimestampException(String.format("Request time has expired %s", requestTimestamp));
        }
    }

    /**
     * Apply the given remote invocation to the given target object, wrapping
     * the invocation result in a serializable RemoteInvocationResult object.
     * The default implementation creates a plain RemoteInvocationResult.
     * <p>Can be overridden in subclasses for custom invocation behavior,
     * for example to return additional context information. Note that this
     * is not covered by the RemoteInvocationExecutor strategy!
     *
     * @param invocation   the remote invocation
     * @param targetObject the target object to apply the invocation to
     * @return the invocation result
     * @see #invoke
     */
    protected RemoteInvocationResult invokeAndCreateResult(RemoteInvocation invocation, Object targetObject) {
        try {
            Object value = invoke(invocation, targetObject);
            return new RemoteInvocationResult(value);
        } catch (Throwable ex) {
            return new RemoteInvocationResult(ex);
        }
    }

    /**
     * Read a RemoteInvocation from the given HTTP request.
     * <p>Delegates to {@link #readRemoteInvocation(HttpServletRequest, InputStream)} with
     * the {@link HttpServletRequest#getInputStream() servlet request's input stream}.
     *
     * @param request current HTTP request
     * @return the RemoteInvocation object
     * @throws IOException            in case of I/O failure
     * @throws ClassNotFoundException if thrown by deserialization
     */
    protected RemoteInvocation readRemoteInvocation(HttpServletRequest request) throws IOException, ClassNotFoundException {
        MediaType contentType = MediaType.valueOf(request.getContentType());
        if (!MediaType.valueOf(getContentType()).equals(contentType)) {
            throw new InvalidContentTypeException(String.format("request header contentType [%s], server Accept %s", getContentType()));
        }
        return readRemoteInvocation(request, request.getInputStream());
    }

    /**
     *
     * @param request
     * @param is
     * @return
     * @throws IOException
     * @throws ClassNotFoundException
     */
    protected RemoteInvocation readRemoteInvocation(HttpServletRequest request, InputStream is) throws IOException, ClassNotFoundException {
        if (is == null || is.available() <= 0) {
            throw new HttpInvokerException("No method flow in the request");
        }
        ObjectInputStream ois = createObjectInputStream(decorateInputStream(request, is));
        try {
            return remoteInvocationFactory.doReadRemoteInvocation(ois);
        } finally {
            ois.close();
        }
    }

    /**
     * Return the InputStream to use for reading remote invocations,
     * potentially decorating the given original InputStream.
     * <p>The default implementation returns the given stream as-is.
     * Can be overridden, for example, for custom encryption or compression.
     *
     * @param request current HTTP request
     * @param is      the original InputStream
     * @return the potentially decorated InputStream
     * @throws IOException in case of I/O failure
     */
    protected InputStream decorateInputStream(HttpServletRequest request, InputStream is) throws IOException {
        return is;
    }
}
